/******************************************************************************* * Copyright (c) 2007, 2008 Edgar Espina. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * *******************************************************************************/ package org.deved.antlride.viz.dfa; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.deved.antlride.common.ui.AntlrImages; import org.deved.antlride.core.dfa.DFAGraphProvider; import org.deved.antlride.core.dfa.DFAGraphProviderRepository; import org.deved.antlride.core.dot.DotEdge; import org.deved.antlride.core.dot.DotGraph; import org.deved.antlride.core.dot.DotNode; import org.deved.antlride.core.model.IGrammar; import org.deved.antlride.viz.dot.DotFigure; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.dltk.core.builder.ISourceLineTracker; import org.eclipse.dltk.ui.DLTKUIPlugin; import org.eclipse.dltk.utils.TextUtils; import org.eclipse.draw2d.FigureUtilities; import org.eclipse.draw2d.Graphics; import org.eclipse.draw2d.IFigure; import org.eclipse.draw2d.SWTGraphics; import org.eclipse.draw2d.geometry.Rectangle; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.ActionContributionItem; import org.eclipse.jface.action.IAction; import org.eclipse.jface.action.IMenuCreator; import org.eclipse.jface.action.ToolBarManager; import org.eclipse.jface.dialogs.ProgressMonitorDialog; import org.eclipse.jface.operation.IRunnableWithProgress; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.jface.viewers.LabelProvider; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.viewers.ViewerFilter; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.SashForm; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.ImageLoader; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.FileDialog; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Link; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.Text; import org.eclipse.swt.widgets.ToolBar; import org.eclipse.swt.widgets.ToolItem; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.services.IDisposable; import org.eclipse.ui.texteditor.ITextEditor; import org.eclipse.zest.core.widgets.Graph; import org.eclipse.zest.core.widgets.GraphConnection; import org.eclipse.zest.core.widgets.GraphNode; import org.eclipse.zest.core.widgets.IContainer; import org.eclipse.zest.core.widgets.ZestStyles; import org.eclipse.zest.layouts.LayoutAlgorithm; import org.eclipse.zest.layouts.LayoutStyles; import org.eclipse.zest.layouts.algorithms.DirectedGraphLayoutAlgorithm; import org.eclipse.zest.layouts.algorithms.GridLayoutAlgorithm; import org.eclipse.zest.layouts.algorithms.HorizontalTreeLayoutAlgorithm; import org.eclipse.zest.layouts.algorithms.RadialLayoutAlgorithm; import org.eclipse.zest.layouts.algorithms.SpringLayoutAlgorithm; import org.eclipse.zest.layouts.algorithms.TreeLayoutAlgorithm; public class AntlrDFAViewer extends Viewer implements IDisposable { private class ExportAsDot extends Action { public ExportAsDot() { super("Export as dot", IAction.AS_PUSH_BUTTON); } @Override public void run() { FileDialog dialog = new FileDialog(getControl().getShell(), SWT.SAVE); dialog.setFileName(currentGraph.ruleName + "-dec" + currentGraph.decisionNumber + ".dot"); String path = dialog.open(); if (path != null) { File out = new File(path); exportAsDot(out); } } } private class ExportAsImage extends Action { private int format; private String formatName; public ExportAsImage(String formatName, int format) { super("Export as " + formatName, IAction.AS_PUSH_BUTTON); this.formatName = formatName; this.format = format; } @Override public void run() { FileDialog dialog = new FileDialog(getControl().getShell(), SWT.SAVE); dialog.setFileName(currentGraph.ruleName + "-dec" + currentGraph.decisionNumber + "." + formatName); String path = dialog.open(); if (path != null) { File out = new File(path); exportAsImage(out, format); } } } private class LayoutAction extends Action { private Class<? extends LayoutAlgorithm> layoutClass; public LayoutAction(String text, Class<? extends LayoutAlgorithm> layoutClass) { super(text, IAction.AS_RADIO_BUTTON); this.layoutClass = layoutClass; } @Override public void run() { applyLayout(layoutClass); } } private class DropDownMenu extends Action implements IMenuCreator { private Menu fMenu; private Action[] delegates; public DropDownMenu(Action... delegates) { super("", delegates.length > 1 ? IAction.AS_DROP_DOWN_MENU : IAction.AS_PUSH_BUTTON); setMenuCreator(this); this.delegates = delegates; delegates[0].setChecked(true); } public void dispose() { if (fMenu != null) fMenu.dispose(); delegates = null; } public Menu getMenu(Control parent) { fMenu = new Menu(parent); for (int i = 0; i < delegates.length; i++) { ActionContributionItem item = new ActionContributionItem( delegates[i]); item.fill(fMenu, -1); } return fMenu; } public Menu getMenu(Menu parent) { return null; } @Override public void run() { fMenu.setVisible(true); } @Override public void setEnabled(boolean enabled) { for (int i = 0; i < delegates.length; i++) { delegates[i].setEnabled(enabled); } super.setEnabled(enabled); } } private class DFANode extends org.deved.antlride.viz.dot.DotNode { public DFANode(IContainer graphModel, int style, String text) { super(graphModel, style, text); } @Override protected IFigure createFigureForModel() { String shape = currentNode.getAttribute("shape"); DotFigure figure = new DotFigure(currentNode.name, getDefaultSize(currentNode), shape); figure.setBackgroundColor(DEFAULT_NODE_COLOR); figure.setForegroundColor(DARK_BLUE); return figure; } } private class GraphLabelProvider extends LabelProvider { @Override public String getText(Object element) { if (element instanceof String) { return (String) element; } DotGraph graph = (DotGraph) element; return graph.decision; } @Override public Image getImage(Object element) { if (element instanceof String) { return AntlrImages.getImage(AntlrImages.RULE); } return AntlrImages.getImage(AntlrImages.DECISION); } } private class GraphContentProvider implements ITreeContentProvider { private Map<String, List<DotGraph>> graphMap = new LinkedHashMap<String, List<DotGraph>>(); public Object[] getElements(Object element) { return graphMap.keySet().toArray(new Object[0]); } public void dispose() { graphMap.clear(); } public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { graphMap.clear(); if (newInput != null) { DotGraph[] dotGraphs = (DotGraph[]) newInput; for (DotGraph dotGraph : dotGraphs) { List<DotGraph> graphList = graphMap.get(dotGraph.ruleName); if (graphList == null) { graphList = new ArrayList<DotGraph>(); graphMap.put(dotGraph.ruleName, graphList); } graphList.add(dotGraph); } } } public Object[] getChildren(Object parentElement) { return graphMap.get(parentElement).toArray(new Object[0]); } public Object getParent(Object element) { return null; } public boolean hasChildren(Object element) { return String.class.isInstance(element); } } private class GraphSelectionListener implements ISelectionChangedListener { public void selectionChanged(SelectionChangedEvent event) { ISelection selection = event.getSelection(); if (selection.isEmpty()) { clearGraph(); } else { IStructuredSelection ss = (IStructuredSelection) selection; Object element = ss.getFirstElement(); if (element instanceof String) { clearGraph(); } else { draw((DotGraph) element); } } } } private Color DEFAULT_NODE_COLOR = new Color(null, 216, 228, 248); private Color DARK_BLUE = new Color(null, 1, 70, 122); private static final Pattern LINE_NUMBER_PATTERN = Pattern .compile("\\d+:\\d+:"); private Graph graph; private Composite control; private TreeViewer decisionViewer; // TODO: temporary solution, invalid initialization of GraphNode private DotNode currentNode; private DotGraph currentGraph; private DropDownMenu layoutAction; private Link description; private DropDownMenu exportAction; private IGrammar grammar; private ISourceLineTracker lineTracker; public AntlrDFAViewer(Composite composite) { createControl(composite); } public void exportAsDot(File file) { IStructuredSelection selection = (IStructuredSelection) decisionViewer .getSelection(); try { DotGraph g = (DotGraph) selection.getFirstElement(); OutputStream out = new BufferedOutputStream(new FileOutputStream( file)); out.write(g.dot.getBytes()); out.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } public void exportAsImage(File file, int format) { IFigure figure = graph.getContents(); Rectangle r = figure.getBounds(); OutputStream out = null; Image image = null; GC gc = null; Graphics g = null; try { out = new BufferedOutputStream(new FileOutputStream(file)); image = new Image(getControl().getDisplay(), r.width, r.height); gc = new GC(image); g = new SWTGraphics(gc); g.translate(r.x * -1, r.y * -1); figure.paint(g); ImageLoader imageLoader = new ImageLoader(); imageLoader.data = new ImageData[] { image.getImageData() }; imageLoader.save(out, SWT.IMAGE_PNG); } catch (FileNotFoundException e) { e.printStackTrace(); } finally { if (g != null) { g.dispose(); } if (gc != null) { gc.dispose(); } if (image != null) { image.dispose(); } if (out != null) { try { out.close(); } catch (IOException e) { e.printStackTrace(); } } } } protected void applyLayout(Class<? extends LayoutAlgorithm> layoutClass) { try { if (graph != null) { LayoutAlgorithm layoutAlgorithm = layoutClass.newInstance(); layoutAlgorithm.setStyle(LayoutStyles.NO_LAYOUT_NODE_RESIZING); graph.setLayoutAlgorithm(layoutAlgorithm, true); } } catch (InstantiationException e) { graph.setLayoutAlgorithm(new DirectedGraphLayoutAlgorithm( LayoutStyles.NO_LAYOUT_NODE_RESIZING), true); } catch (IllegalAccessException e) { e.printStackTrace(); } } private void clearGraph() { exportAction.setEnabled(false); layoutAction.setEnabled(false); description.setText(""); @SuppressWarnings("unchecked") List<GraphNode> nodes = graph.getNodes(); while (nodes != null && nodes.size() > 0) { GraphNode node = nodes.get(0); if (node != null && !node.isDisposed()) { node.dispose(); } } @SuppressWarnings("unchecked") List<GraphConnection> connections = graph.getConnections(); while (connections != null && connections.size() > 0) { GraphConnection connection = connections.get(0); if (connection != null && !connection.isDisposed()) { connection.dispose(); } } } private void draw(DotGraph dotGraph) { clearGraph(); currentGraph = dotGraph; exportAction.setEnabled(true); layoutAction.setEnabled(true); Matcher matcher = LINE_NUMBER_PATTERN.matcher(dotGraph.description); StringBuilder desc = new StringBuilder(); desc.append("<a>"); desc.append(dotGraph.name); desc.append("</a>: "); String detail = null; String position = null; if (matcher.find()) { position = matcher.group(); detail = dotGraph.description.substring(matcher.end() + 1); } else { detail = dotGraph.description; } desc.append(detail); description.setText(desc.toString()); description.setData(position); Iterator<DotNode> nodes = dotGraph.nodes(); Map<DotNode, GraphNode> m = new HashMap<DotNode, GraphNode>(); // create nodes while (nodes.hasNext()) { currentNode = nodes.next(); GraphNode gnode = new DFANode(this.graph, SWT.NONE, currentNode.name); m.put(currentNode, gnode); } // create connections Iterator<DotEdge> edges = dotGraph.edges(); Color black = Display.getDefault().getSystemColor(SWT.COLOR_BLACK); while (edges.hasNext()) { DotEdge dotEdge = edges.next(); GraphConnection connection = new GraphConnection(graph, SWT.NONE, m .get(dotEdge.from), m.get(dotEdge.to)); connection.setLineColor(black); String label = dotEdge.getAttribute("label"); String[] largeLabel = label.split(","); if (largeLabel.length > 2) { StringBuilder shortLabel = new StringBuilder(largeLabel[0]); shortLabel.append(",...,"); shortLabel.append(largeLabel[largeLabel.length - 1]); connection.setText(shortLabel.toString()); } else { connection.setText(label); } connection.setTooltip(new org.eclipse.draw2d.Label(label)); } m.clear(); graph.applyLayout(); } protected int getDefaultSize(DotGraph dotGraph) { Iterator<DotNode> nodes = dotGraph.nodes(); int defaultSize = Integer.MIN_VALUE; // create nodes while (nodes.hasNext()) { DotNode dotNode = nodes.next(); int size = getDefaultSize(dotNode); if (size > defaultSize) { defaultSize = size; } } return defaultSize + 5; } private int getDefaultSize(DotNode dotNode) { Font font = getDefaultFont(); int size = FigureUtilities.getTextExtents(dotNode.name + " ", font).width; return size + 10; } private Font getDefaultFont() { return Display.getDefault().getSystemFont(); } private void createControl(Composite composite) { GridData gd; SashForm sashForm = new SashForm(composite, SWT.HORIZONTAL); Composite leftPane = new Composite(sashForm, SWT.NONE); leftPane.setLayout(new GridLayout(3, false)); Label label = new Label(leftPane, SWT.NONE); label.setText("Rule:"); label.setToolTipText("Search Rule"); final Text decisionText = new Text(leftPane, SWT.BORDER); gd = new GridData(GridData.FILL_HORIZONTAL); decisionText.setLayoutData(gd); decisionText.setToolTipText("Search Rule"); decisionText.addModifyListener(new ModifyListener() { public void modifyText(ModifyEvent e) { decisionViewer.refresh(); } }); ToolItem clearFilter = new ToolItem(new ToolBar(leftPane, SWT.HORIZONTAL), SWT.PUSH); clearFilter.setImage(AntlrImages.getImage(AntlrImages.CLEAR)); clearFilter.setToolTipText("Clear"); clearFilter.addSelectionListener(new SelectionListener() { public void widgetSelected(SelectionEvent e) { decisionText.setText(""); decisionViewer.refresh(); } public void widgetDefaultSelected(SelectionEvent e) { } }); decisionViewer = new TreeViewer(leftPane); gd = new GridData(GridData.FILL_BOTH); gd.horizontalSpan = 3; decisionViewer.getTree().setLayoutData(gd); decisionViewer.addFilter(new ViewerFilter() { @Override public boolean select(Viewer viewer, Object parentElement, Object element) { String ruleName = decisionText.getText().toLowerCase(); if (ruleName.length() > 0) { String s = null; if (String.class.isInstance(parentElement)) { s = (String) parentElement; } else if (String.class.isInstance(element)) { s = (String) element; } if (s != null) { return s.startsWith(ruleName); } return false; } return true; } }); decisionViewer.setLabelProvider(new GraphLabelProvider()); decisionViewer.setContentProvider(new GraphContentProvider()); decisionViewer .addSelectionChangedListener(new GraphSelectionListener()); Composite rightPane = new Composite(sashForm, SWT.NONE); rightPane.setLayout(new GridLayout(2, false)); description = new Link(rightPane, SWT.NONE); gd = new GridData(GridData.FILL_HORIZONTAL); description.setLayoutData(gd); description.addSelectionListener(new SelectionListener() { public void widgetSelected(SelectionEvent e) { String position = (String) description.getData(); try { if (position != null) { String[] location = position.split(":"); int line = Integer.parseInt(location[0]) - 1; int length = Integer.parseInt(location[1]); // update editor selection IWorkbenchPage page = DLTKUIPlugin.getActivePage(); if (page != null) { IEditorPart part = page.getActiveEditor(); if (part != null) { ITextEditor editor = (ITextEditor) part .getAdapter(ITextEditor.class); int offset = getLineTracker().getLineOffset( line) + length - 1; editor.selectAndReveal(0, 1); editor.selectAndReveal(offset, 1); } } } } catch (Exception ex) { ex.printStackTrace(); } } public void widgetDefaultSelected(SelectionEvent e) { } }); ToolBar bar = new ToolBar(rightPane, SWT.FLAT); gd = new GridData(); gd.horizontalAlignment = SWT.END; bar.setLayoutData(gd); ToolBarManager manager = new ToolBarManager(bar); Action[] exportActions = { new ExportAsDot(), new ExportAsImage("png", SWT.IMAGE_PNG) /* * , new * ExportAsImage("gif", * SWT.IMAGE_GIF), new * ExportAsImage("jpeg", * SWT.IMAGE_JPEG), new * ExportAsImage("bmp", * SWT.IMAGE_BMP) */}; exportAction = new DropDownMenu(exportActions); exportAction.setImageDescriptor(AntlrImages .getDescriptor(AntlrImages.SAVE_AS)); exportAction.setToolTipText("Export graph"); exportAction.setEnabled(false); manager.add(exportAction); Action[] layoutActions = { new LayoutAction("Horizontal Tree Layout", HorizontalTreeLayoutAlgorithm.class), new LayoutAction("Grid Layout", GridLayoutAlgorithm.class), new LayoutAction("Tree Layout", TreeLayoutAlgorithm.class), new LayoutAction("Radial Layout", RadialLayoutAlgorithm.class), new LayoutAction("Spring Layout", SpringLayoutAlgorithm.class) }; layoutAction = new DropDownMenu(layoutActions); layoutAction.setImageDescriptor(AntlrImages .getDescriptor(AntlrImages.LAYOUT)); layoutAction.setEnabled(false); manager.add(layoutAction); manager.update(true); graph = new Graph(rightPane, SWT.NONE); gd = new GridData(GridData.FILL_BOTH); gd.horizontalSpan = 2; graph.setLayoutData(gd); graph.setConnectionStyle(ZestStyles.CONNECTIONS_DIRECTED); graph.setLayoutAlgorithm(new HorizontalTreeLayoutAlgorithm( LayoutStyles.NO_LAYOUT_NODE_RESIZING), false); sashForm.setWeights(new int[] { 30, 70 }); control = sashForm; } @Override public Control getControl() { return control; } @Override public Object getInput() { return null; } @Override public ISelection getSelection() { return null; } @Override public void refresh() { } private ISourceLineTracker getLineTracker() { if (lineTracker == null) { lineTracker = TextUtils.createLineTracker(grammar.getSource()); } return lineTracker; } @Override public void setInput(Object input) { if (grammar != input) { grammar = (IGrammar) input; lineTracker = null; ProgressMonitorDialog dlg = new ProgressMonitorDialog(getControl() .getShell()); try { dlg.run(true, false, new IRunnableWithProgress() { public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { DFAGraphProvider graphProvider = DFAGraphProviderRepository .create(); final DotGraph[] graphs = graphProvider.dfa(monitor, grammar); monitor.done(); Display.getDefault().asyncExec(new Runnable() { public void run() { decisionViewer.setInput(graphs); } }); } }); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } } @Override public void setSelection(ISelection selection, boolean reveal) { } public void dispose() { if (control != null && !control.isDisposed()) { control.dispose(); control = null; } if (graph != null && !graph.isDisposed()) { graph.dispose(); } this.grammar = null; if (DARK_BLUE != null && !DARK_BLUE.isDisposed()) { DARK_BLUE.dispose(); DARK_BLUE = null; } if (DEFAULT_NODE_COLOR != null && !DEFAULT_NODE_COLOR.isDisposed()) { DEFAULT_NODE_COLOR.dispose(); DEFAULT_NODE_COLOR = null; } } }